
from pathlib import Path
from itertools import chain
from string import Formatter
from typing import (
    TypeVar, TypeAlias, TextIO, Annotated, Sequence, MutableMapping, TypedDict,
    Literal, Protocol, Union, cast
)

from dotfilebackup.logger import log
from dotfilebackup.settings import ZERO_HOUR

# from dotfilebackup.datatypes import dotFileDataT

# TODO: add mypy flag to throw error if line with ignore[x] coment does not thorw x


__all__ = [
    'dotFileDataT', 'standAloneValT', 'InterpolationDataT',
    'all_date_time_interpolation_names', 'false_interpolation',
    'date_time_interpolations', 'ParsingError', 'InterpolationError',
    'InterpolationHandler', 'KeyInterpolationHandler',
    'datetime_interpolation_handler', 'parse_s', 'parse_f'
]


T = TypeVar('T')

_dotFileDataT:TypeAlias  = Union[
    str, list[str],  dict[str, str], list['_dotFileDataT'], dict[str, '_dotFileDataT']
]
dotFileDataT:TypeAlias  = dict[str, _dotFileDataT]

standAloneValT = Annotated[Sequence, 'value_on_present, value_on_apsent']

InterpolationDataT = TypedDict('InterpolationDataT', {
    'name'       : str,       # interpolation name (must be unique)
    'parser_key' : str|bool,  # parser key name used to update 'val'/'format'
                              #     str   -> name for parser key
                              #     (must be unique in 'parser_keys')
                              #     (must not exist in interpolations)
                              #     True  -> parser key same as name
                              #     False -> no parser key
                              #     (cannot be referenced as key in parser)
                              #     (name still available as interpolation)
    'updatable'  : bool,      # can this interpolation be updated
                              #     by interpolation_handler.set
    'val'        : str|None,  # interpolation value (will not be formated at all)
    'format'     : str|None,  # template for interpolation value (overwrites 'val')
                              # -------------------------------------------------------------
                              # relationship between 'val' and 'format'
                              # -------------------------------------------------------------
                              # 'val'   | 'format' | result | comment
                              # -------------------------------------------------------------
                              # ''      | ''       | error  | empty format
                              # ''      | None     | error  | empty val
                              # ''      | defined  | ok     | on get 'val'=formated 'format'
                              # None    | ''       | error  | empty format
                              # None    | None     | error  | define one
                              # None    | defined  | ok     | on get 'val'=formated 'format'
                              # defined | ''       | error  | empty format
                              # defined | None     | ok     | --
                              # defined | defined  | ok     | on get 'val'=formated 'format'
                              # -------------------------------------------------------------
                              # (PARSER update 'key: val' will update 'format' if it is defined)
                              # (set method will set 'val'=None if 'format' is defined)

    # TODO: ponder, rename to parser_ignore ?????
    'ignore'     : list[str]|Literal[True]|None,  # list of interpolations to ignore
                                                  #     (set, init, new, only get will fill those)
                                                  #     True -> ignore all interpolations
                                                  #     None -> empty ingores
                                                  #     (relevant only for 'format' field)
                                                  #
                                                  #     (!!! FIELD ONLY RELEVANT IN PARSER)
                                                  #
    'forbidden'  : list[str]|None,                # list of forbidden interpolations
                                                  #     None -> empty forbiddens
                                                  #     (relevant only for 'format' field)
})


forbiden_key_characters = {
    ' ' : 'space, ',
    '\t': 'tab, ',
    '{' : '"{", ',
    '}' : '"}", ',
}
inter_m = '\x11'  # interpolated minus, "key: - endl" -> val=='' and
                  # "key: {-} endl" -> value=='-' to differentiate that
                  # unicode dc1 character is used temporary
inter_s = '\x12'  # interpolating spaces, "key:    endl" -> val=='' -> list
                  # and "key: {s} endl" -> val==' ' to to avoid val.rstrip
                  # unicode dc2 character is used temporary
base_interpolations = {
    'left_curly'  : '{',
    'l'           : '{',
    'right_curly' : '}',
    'r'           : '}',
    'colon'       : ':',
    'c'           : ':',
    'hash'        : '#',
    'h'           : '#',
    'space'       : inter_s,
    's'           : inter_s,
    'hyphen'      : inter_m,
    'minus'       : inter_m,
    'm'           : inter_m,
}
all_date_time_interpolation_names = [
    'datetime', 'timestamp', 'time', 'hour','min','sec',
    'date', 'day','day_name','day_short','weekday', 'month',
    'month_name', 'month_short', 'year', 'year_short',
]
# meant to be used like
# >>> item = interpolation_handler.get(... , false_interpolation)
# >>> if item['name']: ...
# >>> if item['updatable']: ...
false_interpolation:InterpolationDataT = {
        'name'       : '',
        'parser_key' : False,
        'updatable'  : False,
        'val'        : '',
        'format'     : None,
        'ignore'     : None,
        'forbidden'  : None,
}
date_time_interpolations:list[InterpolationDataT] = [
    {
        'name'       : 'datetime',
        'parser_key' : 'datetime_format',
        'updatable'  : True,
        'val'        : None,
        'format'     : '{date}_{time}',
        'ignore'     : all_date_time_interpolation_names,
        'forbidden'  : None,
    },
    {
        'name'       : 'date',
        'parser_key' : 'date_format',
        'updatable'  : True,
        'val'        : None,
        'format'     : '{day}.{month}.{year}',
        'ignore'     : all_date_time_interpolation_names,
        'forbidden'  : ['datetime', 'time'],
    },
    {
        'name'       : 'time',
        'parser_key' : 'time_format',
        'updatable'  : True,
        'val'        : None,
        'format'     : '{hour}-{min}-{sec}',  # ':' is restricted in filename on win
        'ignore'     : all_date_time_interpolation_names,
        'forbidden'  : ['datetime', 'date'],
    },
    {
        'name'       : 'timestamp',
        'parser_key' : False,
        'updatable'  : False,
        'val'        : ZERO_HOUR.TIMESTAMP,
        'format'     : None,
        'ignore'     : None,
        'forbidden'  : None,
    },
    {
        'name'       : 'hour',
        'parser_key' : False,
        'updatable'  : False,
        'val'        : ZERO_HOUR.HOUR,
        'format'     : None,
        'ignore'     : None,
        'forbidden'  : None,
    },
    {
        'name'       : 'min',
        'parser_key' : False,
        'updatable'  : False,
        'val'        : ZERO_HOUR.MIN,
        'format'     : None,
        'ignore'     : None,
        'forbidden'  : None,
    },
    {
        'name'       : 'sec',
        'parser_key' : False,
        'updatable'  : False,
        'val'        : ZERO_HOUR.SEC,
        'format'     : None,
        'ignore'     : None,
        'forbidden'  : None,
    },
    {
        'name'       : 'day',
        'parser_key' : False,
        'updatable'  : False,
        'val'        : ZERO_HOUR.DAY,
        'format'     : None,
        'ignore'     : None,
        'forbidden'  : None,
    },
    {
        'name'       : 'day_name',
        'parser_key' : False,
        'updatable'  : False,
        'val'        : ZERO_HOUR.DAY_NAME,
        'format'     : None,
        'ignore'     : None,
        'forbidden'  : None,
    },
    {
        'name'       : 'day_short',
        'parser_key' : False,
        'updatable'  : False,
        'val'        : ZERO_HOUR.DAY_SHORT,
        'format'     : None,
        'ignore'     : None,
        'forbidden'  : None,
    },
    {
        'name'       : 'weekday',
        'parser_key' : False,
        'updatable'  : False,
        'val'        : ZERO_HOUR.WEEKDAY,
        'format'     : None,
        'ignore'     : None,
        'forbidden'  : None,
    },
    {
        'name'       : 'month',
        'parser_key' : False,
        'updatable'  : False,
        'val'        : ZERO_HOUR.MONTH,
        'format'     : None,
        'ignore'     : None,
        'forbidden'  : None,
    },
    {
        'name'       : 'month_name',
        'parser_key' : False,
        'updatable'  : False,
        'val'        : ZERO_HOUR.MONTH_NAME,
        'format'     : None,
        'ignore'     : None,
        'forbidden'  : None,
    },
    {
        'name'       : 'month_short',
        'parser_key' : False,
        'updatable'  : False,
        'val'        : ZERO_HOUR.MONTH_SHORT,
        'format'     : None,
        'ignore'     : None,
        'forbidden'  : None,
    },
    {
        'name'       : 'year',
        'parser_key' : False,
        'updatable'  : False,
        'val'        : ZERO_HOUR.YEAR,
        'format'     : None,
        'ignore'     : None,
        'forbidden'  : None,
    },
    {
        'name'       : 'year_short',
        'parser_key' : False,
        'updatable'  : False,
        'val'        : ZERO_HOUR.YEAR_SHORT,
        'format'     : None,
        'ignore'     : None,
        'forbidden'  : None,
    },
]


class ParsingError(Exception): pass
class InterpolationError(ParsingError): pass


class InterpolationHandler(Protocol):
    parser_keys:dict[str, str]  # dict[parser_key, interpolation_name]

    def __init__(self, items:list[InterpolationDataT]|None=None) -> None:
        ...
    def new(self, items:list[InterpolationDataT]|None=None) -> 'InterpolationHandler':
        ...
    def set(self, data:InterpolationDataT) -> None:
        ...
    def get(self, key:str, default:T|None=None) -> InterpolationDataT|None|T:
        ...




# TODO: poneder keyInterpolationHandler  -> BaseInterpolationHandler
#       or      InterpolationHandler     -> BaseInterpolationHandler
#       and     BaseInterpolationHandler -> InterpolationHandler
class KeyInterpolationHandler(InterpolationHandler):
    def __init__(self, items:list[InterpolationDataT]|None=None) -> None:
        self.items:dict[str, InterpolationDataT] = {}
        self.parser_keys:dict[str, str] = {}  # dict[parser_key, interpolation_name]
        if items: self._handle(items)  # checks and adds to self.items amd self.parser_keys

    def new(self, items:list[InterpolationDataT]|None=None) -> 'KeyInterpolationHandler':
        if not items:
            new_handler = KeyInterpolationHandler()
            new_handler.parser_keys = self.parser_keys.copy()
            new_handler.items = self.items.copy()
            return new_handler
        else:
            old_items = self.items.copy()
            old_parser_keys = self.parser_keys.copy()
            for new_item in items:
                new_item_name =  new_item.get('name')
                if new_item_name in old_items:
                    old_item = old_items.get(new_item_name)
                    old_item = cast(InterpolationDataT, old_item)
                    old_parser_key = old_item['parser_key']
                    if old_parser_key is True: old_parser_key = new_item_name
                    if old_parser_key: del old_parser_keys[old_parser_key]
                    del old_items[new_item_name]
            new_handler = KeyInterpolationHandler()
            new_handler.parser_keys = old_parser_keys
            new_handler.items = old_items
            new_handler._handle(items)
            return new_handler

    def set(self, item:InterpolationDataT) -> None:
        name = item.get('name', object)
        if name is object: raise InterpolationError(f"???[name] is missing")
        name = cast(str, name)
        old_item = self.items.get(name)
        if old_item:
            if old_item['updatable'] == False:
                raise InterpolationError(f"'{name}' interpolation is non-updatable")
            old_parser_key = old_item['parser_key']
            if old_parser_key:
                del self.parser_keys[name if old_parser_key is True else old_parser_key]
            del self.items[name]
        self._handle([item])

    # TODO: @cache ???
    def get(self, name:str, default:T|None=None) -> InterpolationDataT|None|T:
        item = self.items.get(name, None)
        if not item: return default
        item = item.copy()
        item_format = item['format']
        if item_format:
            names = [k for _, k, _, _ in Formatter().parse(item_format) if k]
            names_values = {name: self.get(name)['val'] for name in names}  # type: ignore[index]
                                            # validation makes sure the 'names' are in self.items
            item['val'] = item_format.format(**names_values)
        return item

    # later version ???
    # def del(self, name:str) -> None: ...

    # later version ???
    # def update(self, items:list[InterpolationDataT]) -> None: ...

    def _handle(self, items:list[InterpolationDataT]|None) -> None:
        if items is None: return None
        t_none = type(None)
        all_errs:list[str] = []
        errs:list[str] = []
        for item in items:
            format_keys:list[str]|None = None
            nick = '???'
            errs = []

            name         = item['name']       if 'name'       in item else object
            parser_key   = item['parser_key'] if 'parser_key' in item else object
            updatable    = item['updatable']  if 'updatable'  in item else object
            val          = item['val']        if 'val'        in item else object
            format       = item['format']     if 'format'     in item else object
            ignore       = item['ignore']     if 'ignore'     in item else object
            forbidden    = item['forbidden']  if 'forbidden'  in item else object
            t_name       = type(name)
            t_parser_key = type(parser_key)
            t_updatable  = type(updatable)
            t_val        = type(val)
            t_format     = type(format)
            t_ignore     = type(ignore)
            t_forbidden  = type(forbidden)

            ### pre-insert validation ###
            if name is not object:
                if t_name is not str: errs.append(
                    f"{nick}[name] is {t_name.__name__} expected str")
                elif name == '': errs.append(
                    f"{nick}[name] is empty expected something anything")
                elif name in self.items: errs.append(
                      f"{nick}[name]=='{name}' already present")
                elif name in base_interpolations: errs.append(
                    f"{nick}[name]=='{name}' in use by base interpolation")
                else:
                    nick = f"'{name}'"

            if name       is object: errs.append(f"???[name] is missing")
            if parser_key is object: errs.append(f"{nick}[parser_key] is missing")
            if updatable  is object: errs.append(f"{nick}[updatable] is missing")
            if val        is object: errs.append(f"{nick}[val] is missing")
            if format     is object: errs.append(f"{nick}[format] is missing")
            if ignore     is object: errs.append(f"{nick}[ignore] is missing")
            if forbidden  is object: errs.append(f"{nick}[forbidden] is missing")

            if parser_key is not object:
                if t_parser_key not in (str, bool): errs.append(
                    f"{nick}[parser_key] is {t_parser_key.__name__} expected str|bool")
                elif t_parser_key is str:
                    if parser_key == '': errs.append(
                        f"{nick}[parser_key] is empty expected something anything")
                    if (parser_key in self.items) or (parser_key in self.parser_keys):
                        parser_key = cast(str, parser_key)
                        errs.append(f"{nick}[parser_key]=='{parser_key}' in use by"
                            f" '{self.parser_keys.get(parser_key) or self.items[name]}'")

            if (updatable is not object) and (t_updatable is not bool): errs.append(
                f"{nick}[updatable] is {t_updatable.__name__} expected bool")

            if val is not object:
                if t_val not in (str, t_none): errs.append(
                    f"{nick}[val] is {t_val.__name__} expected str|None")
                elif val == '' and format is None: errs.append(
                    f"{nick}[val] is empty expected something anything")

            if format is not object:
                if t_format not in (str, t_none): errs.append(
                    f"{nick}[format] is {t_format.__name__} expected str|None")
                elif format == '': errs.append(
                    f"{nick}[format] is empty expected something anything")
                elif format is None and val is None: errs.append(
                    f"{nick}[val]==None and {nick}[format]==None"
                    " expected ONE to be something anything")
                elif t_format is str:
                    format = cast(str, format)
                    if f'{{{name}}}' in format: errs.append(
                        f"{nick}[format]=='...{{{name}}}...' self-reference")

            if ignore is not object:
                if ignore is False:
                    errs.append(f"{nick}[ignore]==False expected list[str]|True|None")
                elif t_ignore not in (list, t_none, bool):
                    errs.append(f"{nick}[ignore] is {t_ignore.__name__} expected"
                                " list[str]|True|None")

            if forbidden is not object:
                if t_forbidden not in (list, t_none): errs.append(
                    f"{nick}[forbidden] is {t_forbidden.__name__} expected list[str]|None")
                elif forbidden and t_format is str:
                    format = cast(str, format)
                    for key in [k for _, k, _, _ in Formatter().parse(format) if k]:
                        if key in forbidden: errs.append(
                            f"{nick}[format]=='...{{{key}}}...' forbidden")

            if not errs:
                ### pre-insert modifications ###
                # delete parser-key assosiated with old item
                old_item = self.items.get(name, None)
                if old_item:
                    if type(old_item['parser_key']) is str:
                        del self.parser_keys[old_item['parser_key']]
                    elif old_item['parser_key'] is True:
                        del self.parser_keys[name]
                # add new praser key
                if t_parser_key is str:
                    parser_key = cast(str, parser_key)
                    self.parser_keys[parser_key] = name
                elif parser_key is True:
                    self.parser_keys[name] = name

                self.items[name] = item
            else:
                all_errs.extend(errs)

        ### post-insert-all-items validaton ###  # not need to mange 'items' ordering
        for item in items:
            errs = []
            name = item.get('name')  # type: ignore[assignment]
            format = item.get('format')
            if format and type(format) is str:
                for key in [k for _, k, _, _ in Formatter().parse(format) if k]:
                    if key not in self.items: errs.append(
                        f"'{name}'[format]=='...{{{key}}}...' not available")

            ignore = item.get('ignore')
            if type(ignore) is list:
                for key in ignore:
                    if (key not in self.items): errs.append(
                        f"'{name}'[ignore]==[... '{key}', ...] not available")

            if not errs:
                try: self.get(name)
                except RecursionError as e: errs.append(
                    f"'{name}'[format]=='{format}' infinite recursion")

            all_errs.extend(errs)

        if all_errs: raise InterpolationError('; '.join(all_errs))


datetime_interpolation_handler = KeyInterpolationHandler(date_time_interpolations)


def parse_s(
    text:str|TextIO,
    standalone_keys:dict[str, standAloneValT]|None=None,
    interpolations:InterpolationHandler=datetime_interpolation_handler,
) -> dotFileDataT:
    '''
    # basic idea is

    in_indent = True             # each line starts with indentation >= 0
    current = []
    for ch in text:              # read single chartacte at a time
        ch == endl:              # handle what happens wthen endline is reached
            if in_val:           #     while processing val
                ...
            if in_key:           #     while processing key
                ...
            # if in_ind:         #     while processing indent, line must be
            #     ...            #     empty +- comments, section can be skipped
            reset relevant values
            continue

        if in comment: continue  # skip chars until endl when processing comment
                                 # endline charactrer check must be done first
        ch=='#':
            in_comment = True
            continue

        if in_val:               # process value
            ...
        elif in_key:             # process key
            ...
        elif in_indent:          # process indent
            ...

        current.append(ch)
    '''
    def line_err(msg:str, l_num:int|None=None) -> None:
        where = f'{line_num}:{char_num}' if l_num is None else l_num
        raise ParsingError(f'line {where} {msg}')

    standalone_keys = {} if standalone_keys is None else standalone_keys
    conf:dotFileDataT = {}

    line_num = 1
    char_num = 0
    inde_num = 0    # indentation number
    exp_inde = 0    # expected indent
    in_inde = True  # in_indent, every line begins with indentation >= 0
    in_val = False
    in_key = False
    in_com = False  # in_comment
    in_raw = False  # in_raw_val
    in_list = False
    in_inter = False
    current:list[str] = []
    current_key = ''
    current_list = []
    current_inter:list[str] = []
    for ch in chain(text, '\n'):
        char_num += 1

        if ch == '\n':
            if in_val:
                if current and current[0] == ' ': current = current[1:]  # rm 1st space "key: val"
                val = ''.join(current).rstrip()  # '   '.rstrip() -> ''

                if val:
                    if in_list:
                        val = val.replace(inter_m, '-').replace(inter_s, ' ')
                        current_list.append(val)
                    else:
                        if val == '-': val = ''  # remove empty value marker
                        val = val.replace(inter_m, '-').replace(inter_s, ' ')
                        parser_key = interpolations.parser_keys.get(current_key, object)
                        if parser_key is not object:
                            parser_key = cast(str, parser_key)
                            inter_item = interpolations.get(parser_key)
                            inter_item = cast(InterpolationDataT, inter_item)  # guaranteed to exist
                            if inter_item['format']: inter_item['format'] = val
                            else: inter_item['val'] = val
                            try: interpolations.set(inter_item)
                            except InterpolationError as err: raise err
                        else:
                            conf[current_key] = val
                        current_key = ''

                else:  # begginning of list or err
                    if in_list: line_err('empty value in list')
                    in_list = True
                    conf[current_key] = current_list
            elif in_key:  # standalone_keys-key or error
                key = ''.join(current).rstrip()
                val:str|object = standalone_keys.pop(key, object)  # type: ignore[no-redef]
                if val is object: line_err(f'unknown standalone key "{key}"')
                conf[key] = val[0]
            # if in_inde: pass  # empty line

            if not in_list: in_raw = False
            in_inde  = True ;    line_num += 1
            in_val   = False;    char_num  = 0
            in_key   = False;    inde_num  = 0
            in_com   = False;    current   = []
            in_inter = False
            continue

        if in_raw: pass
        elif in_com: continue
        elif ch == '#': in_com = True; continue

        if in_val:
            if in_raw:
                pass
            elif ch == '{':
                if in_inter: line_err('interpolation contains "{"')
                in_inter = True
                continue
            elif ch == '}':
                if not in_inter: line_err('not opened interpolation')
                inter_key = ''.join(current_inter)
                base_inter = base_interpolations.get(inter_key, object)
                if base_inter is not object:
                    base_inter = cast(str, base_inter)
                    current.append(base_inter)
                else:
                    inter_parser_item = interpolations.get(current_key, false_interpolation)
                    inter_parser_item = cast(InterpolationDataT, inter_parser_item)
                    ignores = inter_parser_item['ignore']
                    inter = interpolations.get(inter_key, object)
                    if inter is object: line_err(f'unknown interpolation "{inter_key}"')
                    inter = cast(InterpolationDataT, inter)
                    if ignores and (ignores is True or inter_key in ignores):
                        current.append(f'{{{inter_key}}}')
                    else:
                        val = cast(str, inter['val'])  # interpolations.get will fill 'val' with str
                        current.append(val)
                current_inter = []
                in_inter = False
                continue
            elif in_inter:
                current_inter.append(ch)
                continue
        elif in_key:
            if ch == ':':
                in_val = True
                key = ''.join(current).rstrip()
                if key.startswith("r'"):
                    in_raw = True
                    key = key[2:]
                errs = [forbiden_key_characters[c] for c in key
                        if c in forbiden_key_characters]
                if errs: line_err('key contains ' + ''.join(errs))
                if key in conf: line_err(f'duplicated key "{key}"')
                current_key = key
                current = []
                continue
        elif in_inde:
            if ch == ' ':
                inde_num += 1
                continue
            elif ch == '\t':
                line_err('tab in indentation')
            elif ch == ':':
                line_err('empty key')
            elif ch == '-':
                if not in_list: line_err(
                    'key begins with "-", tip: in r\'keys: # comment is value')
                if inde_num == 0 and not exp_inde: line_err('unindented list')

                if exp_inde == 0:
                    exp_inde = inde_num  # first element sets indentation
                elif exp_inde != inde_num:
                    line_err(f'expected {exp_inde} spaces indentation')
                # else: pass  # indentation ok
                in_val = True
                continue
            else:
                if inde_num != 0:
                    line_err('unexpected indentation')
                elif in_list and not current_list:
                    line_err(f'use "{current_key}: -" for empty value', line_num-1)

                current_list = []
                exp_inde = 0
                in_list = False
                if in_raw and ch == '#':  # comment exits raw mode list
                    in_com = True;        # r'this:
                    in_raw = False        #     - exact
                    continue              # # situation
                in_key = True
                in_raw = False

        current.append(ch)

    if in_list and conf[current_key] == []:  # last "line": 'key: ' -> {current_key: []}
        line_err(f'use "{current_key}: -" for empty value' , -1)

    for k, v, in standalone_keys.items():
        on_present, on_absent = v
        conf[k] = on_absent
    return conf


def parse_f(
    file:str|Path,
    standalone_keys:dict[str, standAloneValT]|None=None
) -> dotFileDataT:
    with open(file, 'r', encoding='utf-8') as f:
        log.info(f'opening {file}')
        try:
            return parse_s(f.read(), standalone_keys=standalone_keys)
        except (OSError, ParsingError) as e:
            log.error(f'{file} {e.__class__.__name__}: {e}')
            raise e

#
